// ignore: implementation_imports
import 'package:build/src/builder/build_step.dart';
import 'package:analyzer/dart/element/element.dart' as ele;
import 'package:analyzer/dart/constant/value.dart';

// ignore: import_of_legacy_library_into_null_safe
import 'package:code_builder/code_builder.dart';
import 'package:source_gen/source_gen.dart';
import 'package:build/build.dart';
import 'dart:io';
import 'dart:ffi';
import 'package:tuple/tuple.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:dio/dio.dart';
import 'package:built_collection/built_collection.dart';

// ignore: import_of_legacy_library_into_null_safe
import 'package:code_builder/code_builder.dart' as cb;
import 'package:dart_style/dart_style.dart';
import 'package:source_gen/source_gen.dart' as sg;
import 'package:xanno/xanno.dart' as http;

class HttpInterfaceGenerator extends GeneratorForAnnotation<http.Interface> {
  final separator = Platform.pathSeparator;
  final _methodsAnnotations = const [
    http.GET,
    http.POST,
    http.PATCH,
    http.PUT,
    http.DELETE,
    http.HEAD,
    http.OPTIONS,
  ];

  bool _isBasicType(DartType? returnType) {
    if (returnType == null) return false;
    return _typeChecker(String).isExactlyType(returnType) ||
        _typeChecker(bool).isExactlyType(returnType) ||
        _typeChecker(int).isExactlyType(returnType) ||
        _typeChecker(double).isExactlyType(returnType) ||
        _typeChecker(num).isExactlyType(returnType) ||
        _typeChecker(Double).isExactlyType(returnType) ||
        _typeChecker(Float).isExactlyType(returnType);
  }

  TypeChecker _typeChecker(Type type) => TypeChecker.fromRuntime(type);

  cb.Method? _generateMethod(MethodElement m) {
    ConstantReader? httpMehod = _getMethodAnnotation(m);

    if (httpMehod == null) {
      return null;
    }
    return cb.Method((mm) {
      mm
        ..returns = refer(_displayString(m.type.returnType))
        ..name = m.displayName
        ..types.addAll(m.typeParameters.map((e) => refer(e.name)))
        ..modifier = m.returnType.isDartAsyncFuture
            ? MethodModifier.async
            : MethodModifier.asyncStar
        ..annotations.add(CodeExpression(Code('override')));

      /// required parameters
      mm.requiredParameters.addAll(m.parameters
          .where((it) => it.isRequiredPositional || it.isRequiredNamed)
          .map((it) => Parameter((p) => p
            ..name = it.name
            ..named = it.isNamed
            ..type = refer(it.type.getDisplayString(withNullability: true)))));

      /// optional positional or named parameters
      mm.optionalParameters
          .addAll(m.parameters.where((i) => i.isOptional).map((it) {
        return Parameter((p) => p
          ..name = it.name
          ..named = it.isNamed
          ..type = refer(it.type.getDisplayString(withNullability: true))
          ..defaultTo =
              it.defaultValueCode == null ? null : Code(it.defaultValueCode));
      }));
      mm.body = _generateRequest(m, httpMehod);
    });
  }

  void _generateQueries(
      MethodElement m, List<Code> blocks, String _queryParamsVar) {
    final queries = _getAnnotations(m, http.Query);
    final queryParameters = queries.map((p, ConstantReader r) {
      final key = r.peek("value")?.stringValue ?? p.displayName;
      var value;
      if (_isBasicType(p.type)) {
        value = refer(p.displayName);
      } else if (p.type.isDartCoreList) {
        value = refer(p.displayName);
      } else if (p.type.isDartCoreMap) {
        value = refer(p.displayName);
      }
      return MapEntry(literalString(key, raw: true), value);
    });

    final queryMap = _getAnnotations(m, http.Queries);
    blocks.add(literalMap(queryParameters, refer("String"), refer("dynamic"))
        .assignFinal(_queryParamsVar)
        .statement);
    for (final p in queryMap.keys) {
      final type = p.type;
      final displayName = p.displayName;
      var value;
      if (_isBasicType(type)) {
        value = refer(displayName);
      } else if (type.isDartCoreList) {
        value = refer(displayName);
      } else if (type.isDartCoreMap) {
        value = refer(displayName);
      } else {
        final ele = type.element as ClassElement;
        final toJson = ele.lookUpMethod('toJson', ele.library);
        if (toJson == null) {
          value = refer("jsonEncode($displayName)");
        } else {
          value = refer("$displayName?.toJson()");
        }
      }

      /// workaround until this is merged in code_builder
      /// https://github.com/dart-lang/code_builder/pull/269
      final emitter = DartEmitter();
      final buffer = StringBuffer();
      if (value != null) {
        value.accept(emitter, buffer);
        if (p.isOptional) {
          refer('?? <String,dynamic>{}').accept(emitter, buffer);
        }
        final expression = refer(buffer.toString());

        blocks
            .add(refer('$_queryParamsVar.addAll').call([expression]).statement);
      }
    }

    if (m.parameters
        .where((p) => (p.isOptional && !p.isRequiredNamed))
        .isNotEmpty) {
      blocks.add(Code("$_queryParamsVar.removeWhere((k, v) => v == null);"));
    }
  }

  ConstantReader? _getHeadersAnnotation(MethodElement method) {
    final annot = _typeChecker(http.Headers)
        .firstAnnotationOf(method, throwOnUnresolved: false);
    if (annot != null) return ConstantReader(annot);
    return null;
  }

  Map<String, Expression> _generateHeaders(MethodElement m) {
    ConstantReader? anno = _getHeadersAnnotation(m);
    Map<DartObject?, DartObject?> headersMap;
    try {
      headersMap = anno!.peek("value")!.mapValue;
    } catch (e) {
      headersMap = {};
    }
    final headers = headersMap.map((k, v) {
      var newK = '', newV = '';
      if (k != null) {
        newK = k.toStringValue() ?? '';
      }
      if (v != null) {
        newV = v.toStringValue() ?? '';
      }
      return MapEntry(newK, literal(newV));
    });

    final annosInParam = _getAnnotations(m, http.Header);
    final headersInParams = annosInParam.map((k, v) {
      String value;
      try {
        value = v.peek("value")!.stringValue;
      } catch (e) {
        value = k.displayName;
      }
      return MapEntry(value, refer(k.displayName));
    });
    headers.addAll(headersInParams);
    return headers;
  }

  Tuple2<ParameterElement, ConstantReader>? _getAnnotation(
      MethodElement m, Type type) {
    for (final p in m.parameters) {
      final a = _typeChecker(type).firstAnnotationOf(p);
      if (a != null) {
        return Tuple2(p, ConstantReader(a));
      }
    }
    return null;
  }

  DartType? _genericOf(DartType type) {
    return type is InterfaceType && type.typeArguments.isNotEmpty
        ? type.typeArguments.first
        : null;
  }

  bool _isBasicInnerType(DartType returnType) {
    var innnerType = _genericOf(returnType);
    return innnerType == null ? false : _isBasicType(innnerType);
  }

  ConstantReader? _getFormUrlEncodedAnnotation(MethodElement method) {
    final annotation = _typeChecker(http.FormUrlEncoded)
        .firstAnnotationOf(method, throwOnUnresolved: false);
    if (annotation != null) return ConstantReader(annotation);
    return null;
  }

  void _generateRequestBody(
      List<Code> blocks, String _dataVar, MethodElement m) {
    final _bodyName = _getAnnotation(m, http.Body)?.item1;
    if (_bodyName != null) {
      if (TypeChecker.fromRuntime(Map).isAssignableFromType(_bodyName.type)) {
        blocks.add(literalMap({}, refer("String"), refer("dynamic"))
            .assignFinal(_dataVar)
            .statement);

        if (_bodyName.isOptional) {
          blocks.add(refer("$_dataVar.addAll").call([
            refer("${_bodyName.displayName} ?? <String,dynamic>{}")
          ]).statement);
        } else {
          blocks.add(refer("$_dataVar.addAll")
              .call([refer("${_bodyName.displayName}")]).statement);
        }

        blocks.add(Code("$_dataVar.removeWhere((k, v) => v == null);"));
      } else if ((_typeChecker(List).isExactly(_bodyName.type.element!) ||
              _typeChecker(BuiltList).isExactly(_bodyName.type.element!)) &&
          !_isBasicInnerType(_bodyName.type)) {
        blocks.add(refer('''
            ${_bodyName.displayName}.map((e) => e.toJson()).toList()
            ''').assignFinal(_dataVar).statement);
      } else if (_typeChecker(File).isExactly(_bodyName.type.element!)) {
        if (_bodyName.isOptional) {
          blocks.add(Code("var data;"));
          blocks.add(Code("if (${_bodyName.displayName} != null) {"));
          blocks.add(refer(_dataVar)
              .assign(refer("Stream").property("fromIterable").call([
                refer(
                    "${_bodyName.displayName}.readAsBytesSync().map((i)=>[i])")
              ]).expression)
              .statement);
          blocks.add(Code("}"));
        } else {
          blocks.add(refer("Stream")
              .property("fromIterable")
              .call([
                refer(
                    "${_bodyName.displayName}.readAsBytesSync().map((i)=>[i])")
              ])
              .assignFinal(_dataVar)
              .statement);
        }
      } else if ((_typeChecker(List).isExactly(_bodyName.type.element!) ||
              _typeChecker(BuiltList).isExactly(_bodyName.type.element!)) &&
          _isBasicInnerType(_bodyName.type)) {
        blocks.add(refer('''
            ${_bodyName.displayName}.map((e) => e).toList()
            ''').assignFinal(_dataVar).statement);
      } else if (_bodyName.type.element is ClassElement &&
          !_isBasicInnerType(_bodyName.type)) {
        blocks.add(refer('''
            ${_bodyName.displayName}.toJson()
            ''').assignFinal(_dataVar).statement);
      } else {
        /// @Body annotations with no type are assinged as is
        blocks
            .add(refer(_bodyName.displayName).assignFinal(_dataVar).statement);
      }
      return;
    }

    final fields = _getAnnotations(m, http.Field).map((p, r) {
      final fieldName = r.peek("value")?.stringValue ?? p.displayName;
      final isFileField = _typeChecker(File).isAssignableFromType(p.type);
      if (isFileField) {
        log.severe(
            'File is not support by @Field(). Please use @Part() instead.');
      }
      return MapEntry(literal(fieldName), refer(p.displayName));
    });

    if (fields.isNotEmpty) {
      blocks.add(literalMap(fields).assignFinal(_dataVar).statement);
      blocks.add(Code("$_dataVar.removeWhere((k, v) => v == null);"));
      return;
    }

    final parts = _getAnnotations(m, http.Part);
    if (parts.isNotEmpty) {
      blocks.add(
          refer('FormData').newInstance([]).assignFinal(_dataVar).statement);

      parts.forEach((p, r) {
        final fieldName = r.peek("name")?.stringValue ??
            r.peek("value")?.stringValue ??
            p.displayName;
        final isFileField = _typeChecker(File).isAssignableFromType(p.type);
        final contentType = r.peek('contentType')?.stringValue;

        if (isFileField) {
          final fileName = r.peek("fileName")?.stringValue != null
              ? literalString(r.peek("fileName")?.stringValue)
              : refer(p.displayName)
                  .property('path.split(Platform.pathSeparator).last');

          final uploadFileInfo = refer('$MultipartFile.fromFileSync').call([
            refer(p.displayName).property('path')
          ], {
            'filename': fileName,
            if (contentType != null)
              'contentType':
                  refer("MediaType", 'package:http_parser/http_parser.dart')
                      .property('parse')
                      .call([literal(contentType)])
          });
          bool optinalFile = false;
          try {
            optinalFile = m.parameters
                .firstWhere((pp) => pp.displayName == p.displayName)
                .isOptional;
          } catch (e) {}

          final returnCode =
              refer(_dataVar).property('files').property("add").call([
            refer("MapEntry").newInstance([literal(fieldName), uploadFileInfo])
          ]).statement;
          if (optinalFile) {
            final condication =
                refer(p.displayName).notEqualTo(literalNull).code;
            blocks.addAll(
                [Code("if("), condication, Code(") {"), returnCode, Code("}")]);
          } else {
            blocks.add(returnCode);
          }
        } else if (_displayString(p.type) == "List<int>" ||
            _displayString(p.type) == "List<Uint8>" ||
            _displayString(p.type) == "Uint8List") {
          final fileName = r.peek("fileName")?.stringValue;
          final conType = contentType == null
              ? ""
              : 'contentType: MediaType.parse(${literal(contentType)}),';
          if (p.isOptional) {
            blocks.add(Code("if (${p.displayName} != null) {"));
          }
          blocks.add(refer(_dataVar).property('files').property("add").call([
            refer(''' 
                  MapEntry(
                '$fieldName',
                MultipartFile.fromBytes(${p.displayName},

                filename:${literal(fileName ?? null)},
                    $conType
                    ))
                  ''')
          ]).statement);
          if (p.isOptional) {
            blocks.add(Code("}"));
          }
        } else if (_typeChecker(List).isExactlyType(p.type) ||
            _typeChecker(BuiltList).isExactlyType(p.type)) {
          DartType? innnerType = _genericOf(p.type);

          if (_displayString(innnerType) == "List<int>" ||
              _displayString(innnerType) == "List<Uint8>" ||
              _displayString(innnerType) == "Uint8List") {
            final conType = contentType == null
                ? ""
                : 'contentType: MediaType.parse(${literal(contentType)}),';
            if (p.isOptional) {
              blocks.add(Code("if (${p.displayName} != null) {"));
            }
            blocks
                .add(refer(_dataVar).property('files').property("addAll").call([
              refer(''' 
                  ${p.displayName}.map((i) => MapEntry(
                '$fieldName',
                MultipartFile.fromBytes(i,
                    $conType
                    )))
                  ''')
            ]).statement);
            if (p.isOptional) {
              blocks.add(Code("}"));
            }
          } else if (_isBasicType(innnerType) ||
              _typeChecker(Map).isExactlyType(innnerType!) ||
              _typeChecker(BuiltMap).isExactlyType(innnerType) ||
              _typeChecker(List).isExactlyType(innnerType) ||
              _typeChecker(BuiltList).isExactlyType(innnerType)) {
            if (p.isOptional) {
              blocks.add(Code("if (${p.displayName} != null) {"));
            }
            blocks.add(refer('''
            ${p.displayName}.forEach((i){
              $_dataVar.fields.add(MapEntry(${literal(fieldName)}, jsonEncode(i)));
            })
            ''').statement);
            if (p.isOptional) {
              blocks.add(Code("}"));
            }
          } else if (_typeChecker(File).isExactlyType(innnerType)) {
            final conType = contentType == null
                ? ""
                : 'contentType: MediaType.parse(${literal(contentType)}),';
            if (p.isOptional) {
              blocks.add(Code("if (${p.displayName} != null) {"));
            }
            blocks
                .add(refer(_dataVar).property('files').property("addAll").call([
              refer(''' 
                  ${p.displayName}.map((i) => MapEntry(
                '$fieldName',
                MultipartFile.fromFileSync(i.path,
                    filename: i.path.split(Platform.pathSeparator).last,
                    $conType
                    )))
                  ''')
            ]).statement);
            if (p.isOptional) {
              blocks.add(Code("}"));
            }
          } else if (_typeChecker(MultipartFile).isExactlyType(innnerType)) {
            if (p.isOptional) {
              blocks.add(Code("if (${p.displayName} != null) {"));
            }
            blocks
                .add(refer(_dataVar).property('files').property("addAll").call([
              refer(''' 
                  ${p.displayName}.map((i) => MapEntry(
                '$fieldName',
                i))
                  ''')
            ]).statement);
            if (p.isOptional) {
              blocks.add(Code("}"));
            }
          } else if (innnerType.element is ClassElement) {
            final ele = innnerType.element as ClassElement;
            final toJson = ele.lookUpMethod('toJson', ele.library);
            if (toJson == null) {
              if (p.isOptional) {
                blocks.add(Code("if (${p.displayName} != null) {"));
              }
              blocks
                  .add(refer(_dataVar).property('fields').property("add").call([
                refer("MapEntry").newInstance(
                    [literal(fieldName), refer("jsonEncode(${p.displayName})")])
              ]).statement);
              if (p.isOptional) {
                blocks.add(Code("}"));
              }
            } else {
              if (p.isOptional) {
                blocks.add(Code("if (${p.displayName} != null) {"));
              }
              blocks
                  .add(refer(_dataVar).property('fields').property("add").call([
                refer("MapEntry").newInstance([
                  literal(fieldName),
                  refer("${p.displayName}.toJson() ?? <String,dynamic>{}")
                ])
              ]).statement);
              if (p.isOptional) {
                blocks.add(Code("}"));
              }
            }
          }
        } else if (_isBasicType(p.type)) {
          if (p.isOptional) {
            blocks.add(Code("if (${p.displayName} != null) {"));
          }
          blocks.add(refer(_dataVar).property('fields').property("add").call([
            refer("MapEntry").newInstance([
              literal(fieldName),
              if (_typeChecker(String).isExactlyType(p.type))
                refer(p.displayName)
              else
                refer(p.displayName).property('toString').call([])
            ])
          ]).statement);
          if (p.isOptional) {
            blocks.add(Code("}"));
          }
        } else if (_typeChecker(Map).isExactlyType(p.type) ||
            _typeChecker(BuiltMap).isExactlyType(p.type)) {
          if (p.isOptional) {
            blocks.add(Code("if (${p.displayName} != null) {"));
          }
          blocks.add(refer(_dataVar).property('fields').property("add").call([
            refer("MapEntry").newInstance(
                [literal(fieldName), refer("jsonEncode(${p.displayName})")])
          ]).statement);
          if (p.isOptional) {
            blocks.add(Code("}"));
          }
        } else if (p.type.element is ClassElement) {
          final ele = p.type.element as ClassElement;
          final toJson = ele.lookUpMethod('toJson', ele.library);
          if (toJson == null) {
            blocks.add(refer(_dataVar).property('fields').property("add").call([
              refer("MapEntry").newInstance([
                literal(fieldName),
                refer("jsonEncode(${p.displayName}?? <String,dynamic>{})")
              ])
            ]).statement);
          } else {
            if (p.isOptional) {
              blocks.add(Code("if (${p.displayName} != null) {"));
            }
            blocks.add(refer(_dataVar).property('fields').property("add").call([
              refer("MapEntry").newInstance([
                literal(fieldName),
                refer("${p.displayName}.toJson() ?? <String,dynamic>{}")
              ])
            ]).statement);
            if (p.isOptional) {
              blocks.add(Code("}"));
            }
          }
        } else {
          blocks.add(refer(_dataVar).property('fields').property("add").call([
            refer("MapEntry")
                .newInstance([literal(fieldName), refer(p.displayName)])
          ]).statement);
        }
      });
      return;
    }

    /// There is no body
    blocks.add(literalMap({}, refer("String"), refer("dynamic"))
        .assignFinal(_dataVar)
        .statement);
  }

  ConstantReader? _getResponseTypeAnnotation(MethodElement method) {
    final annotation = _typeChecker(http.DioResponseType)
        .firstAnnotationOf(method, throwOnUnresolved: false);
    if (annotation != null) return ConstantReader(annotation);
    return null;
  }

  Expression _parseOptions(
      MethodElement m,
      Map<String, Expression> namedArguments,
      List<Code> blocks,
      Map<String, Expression> extraOptions) {
    //移除baseUrl
    extraOptions.removeWhere((key, value) => key == 'baseUrl');
    final options = refer("Options").newInstance([], extraOptions);
    final annoOptions = _getAnnotation(m, http.DioOptions);
    if (annoOptions == null) {
      return options;
    } else {
      if (annoOptions.item1.isOptional) {
      } else {}
      String ifCodeExtra, ifCodeHeaders;
      if (annoOptions.item1.isOptional) {
        ifCodeExtra =
            "if (${annoOptions.item1.displayName} != null && ${annoOptions.item1.displayName}.extra != null) {";
        ifCodeHeaders =
            "if (${annoOptions.item1.displayName} != null && ${annoOptions.item1.displayName}.headers != null) {";
      } else {
        ifCodeExtra = "if (${annoOptions.item1.displayName}.extra != null) {";
        ifCodeHeaders =
            "if (${annoOptions.item1.displayName}.headers != null) {";
      }
      blocks.add(Code(ifCodeExtra));
      blocks.add(refer(annoOptions.item1.displayName)
          .property("extra!")
          .property('addAll')
          .call([extraOptions.remove("extra")!]).statement);
      blocks.add(Code("}"));
      //
      blocks.add(Code(ifCodeHeaders));
      blocks.add(refer(annoOptions.item1.displayName)
          .property('headers!')
          .property('addAll')
          .call([extraOptions.remove('headers')!]).statement);
      blocks.add(Code("}"));
      return refer(annoOptions.item1.displayName)
          .property('copyWith')
          .call([], extraOptions);
    }
  }

  DartType? _getResponseType(DartType type) {
    return _genericOf(type);
  }

  DartType _getResponseInnerType(DartType type) {
    final generic = _genericOf(type);
    if (generic == null ||
        _typeChecker(Map).isExactlyType(type) ||
        _typeChecker(BuiltMap).isExactlyType(type)) return type;

//    if (generic.isDynamic) return null;

    if (_typeChecker(List).isExactlyType(type) ||
        _typeChecker(BuiltList).isExactlyType(type)) return generic;

    return _getResponseInnerType(generic);
  }

  void _generateExtra(
      MethodElement m, List<Code> blocks, String localExtraVar) {
    final extra =
        _typeChecker(http.Extra).firstAnnotationOf(m, throwOnUnresolved: false);

    if (extra != null) {
      final c = ConstantReader(extra);
      blocks.add(literalMap(
        c.peek('data')?.mapValue.map((k, v) {
              var newK, newV;
              if (k != null) {
                newK = k.toStringValue();
              } else {
                newK = (throw InvalidGenerationSourceError(
                  'Invalid key for extra Map, only `String` keys are supported',
                  element: m,
                  todo: 'Make sure all keys are of string type',
                ));
              }
              if (v != null) {
                newV = v.toBoolValue() ??
                    v.toDoubleValue() ??
                    v.toIntValue() ??
                    v.toStringValue() ??
                    v.toListValue() ??
                    v.toMapValue() ??
                    v.toSetValue() ??
                    v.toSymbolValue() ??
                    v.toTypeValue();
              } else {
                newV = Code(revivedLiteral(null));
              }
              return MapEntry(newK, newV);
            }) ??
            {},
        refer('String'),
        refer('dynamic'),
      ).assignConst(localExtraVar).statement);
    } else {
      blocks.add(literalMap(
        {},
        refer('String'),
        refer('dynamic'),
      ).assignConst(localExtraVar).statement);
    }
  }

  Code _generateRequest(MethodElement m, ConstantReader httpMehod) {
    final returnAsyncWrapper =
        m.returnType.isDartAsyncFuture ? 'return' : 'yield';
    final defineHost = httpMehod.peek("host")?.stringValue ?? 'host';
    final path = _generatePath(m, httpMehod);
    final blocks = <Code>[];
    _generateExtra(m, blocks, 'extra');
    _generateQueries(m, blocks, 'queryParameters');
    Map<String, Expression> headers = _generateHeaders(m);
    _generateRequestBody(blocks, 'data', m);
    final extraOptions = {
      "method": literal(httpMehod.peek("method")!.stringValue),
      "headers": literalMap(
          headers.map((k, v) => MapEntry(literalString(k, raw: true), v)),
          refer("String"),
          refer("dynamic")),
      "extra": refer("extra"),
    };
    Expression? contentTypeInHeader;
    try {
      contentTypeInHeader = headers.entries
          .firstWhere(
              (i) => "Content-Type".toLowerCase() == i.key.toLowerCase())
          .value;
    } catch (e) {
      contentTypeInHeader = null;
    }
    if (contentTypeInHeader != null) {
      extraOptions["contentType"] = contentTypeInHeader;
    }
    final contentType = _getFormUrlEncodedAnnotation(m);
    if (contentType != null) {
      extraOptions["contentType"] =
          literal(contentType.peek("mime")!.stringValue);
    }
    if (defineHost == 'host') {
      extraOptions["baseUrl"] = refer('host');
    } else {
      extraOptions["baseUrl"] = literal(defineHost);
    }

    ConstantReader? responseType = _getResponseTypeAnnotation(m);
    if (responseType != null) {
      final rsType = ResponseType.values.firstWhere((it) {
        return responseType
            .peek("responseType")!
            .objectValue
            .toString()
            .contains(it.toString().split(".")[1]);
      });

      extraOptions["responseType"] = refer(rsType.toString());
    }
    final namedArguments = <String, Expression>{};
    namedArguments["queryParameters"] = refer("queryParameters");
    namedArguments["options"] =
        _parseOptions(m, namedArguments, blocks, extraOptions);
    namedArguments["data"] = refer("data");

    final cancelToken = _getAnnotation(m, http.CancelRequest);
    if (cancelToken != null)
      namedArguments["cancelToken"] = refer(cancelToken.item1.displayName);

    final sendProgress = _getAnnotation(m, http.SendProgress);
    if (sendProgress != null)
      namedArguments["onSendProgress"] = refer(sendProgress.item1.displayName);

    final receiveProgress = _getAnnotation(m, http.ReceiveProgress);
    if (receiveProgress != null)
      namedArguments["onReceiveProgress"] =
          refer(receiveProgress.item1.displayName);

    final wrapperedReturnType = _getResponseType(m.returnType);

    if (wrapperedReturnType == null ||
        "void" == wrapperedReturnType.toString()) {
      if (defineHost == 'host') {
        blocks.add(
          refer("await HttpManager(host: host!).request")
              .call([path], namedArguments, [refer("void")])
              .statement,
        );
      } else {
        blocks.add(
          refer("await HttpManager(host: '$defineHost').request")
              .call([path], namedArguments, [refer("void")])
              .statement,
        );
      }
      blocks.add(Code("$returnAsyncWrapper null;"));
      return Block.of(blocks);
    }

    final bool isWrappered =
        _typeChecker(http.HttpResponse).isExactlyType(wrapperedReturnType);
    DartType? returnType = isWrappered
        ? _getResponseType(wrapperedReturnType)
        : wrapperedReturnType;
    if (returnType == null || "void" == returnType.toString()) {
      if (isWrappered) {
        if (defineHost == 'host') {
          blocks.add(
            refer("final resp = await HttpManager(host: host!).request")
                .call([path], namedArguments, [refer("void")])
                .statement,
          );
        } else {
          blocks.add(
            refer("final resp = await HttpManager(host: '$defineHost').request")
                .call([path], namedArguments, [refer("void")])
                .statement,
          );
        }
        blocks.add(Code("""
      $returnAsyncWrapper HttpResponse<${returnType.toString()}>(resp.data, resp);
      """));
      } else {
        if (defineHost == 'host') {
          blocks.add(
            refer("await HttpManager(host: host!).request")
                .call([path], namedArguments, [refer("void")])
                .statement,
          );
        } else {
          blocks.add(
            refer("await HttpManager(host: '$defineHost').request")
                .call([path], namedArguments, [refer("void")])
                .statement,
          );
        }
        blocks.add(Code("$returnAsyncWrapper null;"));
      }
    } else {
      final innerReturnType = _getResponseInnerType(returnType);
      if (_typeChecker(List).isExactlyType(returnType) ||
          _typeChecker(BuiltList).isExactlyType(returnType)) {
        if (defineHost == 'host') {
          blocks.add(
            refer("await HttpManager(host: host!).request")
                .call([path], namedArguments,
                    [refer(_displayString(innerReturnType))])
                .assignFinal('resp')
                .statement,
          );
        } else {
          blocks.add(
            refer("await HttpManager(host: '$defineHost').request")
                .call([path], namedArguments,
                    [refer(_displayString(innerReturnType))])
                .assignFinal('resp')
                .statement,
          );
        }
      } else if (_typeChecker(Map).isExactlyType(returnType) ||
          _typeChecker(BuiltMap).isExactlyType(returnType)) {
        if (defineHost == 'host') {
          blocks.add(
            refer("await HttpManager(host: host!).request<Map<String,dynamic>>")
                .call([path], namedArguments)
                .assignFinal('resp')
                .statement,
          );
        } else {
          blocks.add(
            refer("await HttpManager(host: '$defineHost').request<Map<String,dynamic>>")
                .call([path], namedArguments)
                .assignFinal('resp')
                .statement,
          );
        }
      } else {
        if (_isBasicType(returnType)) {
          if (defineHost == 'host') {
            blocks.add(
              refer("await HttpManager(host: host!).request<${_displayString(returnType)}>")
                  .call([path], namedArguments)
                  .assignFinal('resp')
                  .statement,
            );
          } else {
            blocks.add(
              refer("await HttpManager(host: '$defineHost').request<${_displayString(returnType)}>")
                  .call([path], namedArguments)
                  .assignFinal('resp')
                  .statement,
            );
          }
        } else if (returnType.toString() == 'dynamic') {
          if (defineHost == 'host') {
            blocks.add(
              refer("await HttpManager(host: host!).request<dynamic>")
                  .call([path], namedArguments)
                  .assignFinal('resp')
                  .statement,
            );
          } else {
            blocks.add(
              refer("await HttpManager(host: '$defineHost').request<dynamic>")
                  .call([path], namedArguments)
                  .assignFinal('resp')
                  .statement,
            );
          }
        } else {
          if (defineHost == 'host') {
            blocks.add(
              refer("await HttpManager(host: host!).request<${_displayString(returnType)}>")
                  .call([path], namedArguments)
                  .assignFinal('resp')
                  .statement,
            );
          } else {
            blocks.add(
              refer("await HttpManager(host: '$defineHost').request<${_displayString(returnType)}>")
                  .call([path], namedArguments)
                  .assignFinal('resp')
                  .statement,
            );
          }
        }
      }
      if (isWrappered) {
        blocks.add(Code("""
      $returnAsyncWrapper HttpResponse<${returnType.toString()}>(resp.data, resp);
       """));
      } else {
        blocks.add(Code("""
            if (resp.data is BaseEntity) {
              $returnAsyncWrapper resp.data.data as ${returnType.toString()};
            } else {
              if (resp.data is DioError) {
                throw resp.data;
              } else {
                $returnAsyncWrapper resp.data as ${returnType.toString()};
              }
            }
        """));
      }
    }
    return Block.of(blocks);
  }

  Expression _generatePath(MethodElement m, ConstantReader method) {
    final paths = _getAnnotations(m, http.Path);
    String definePath = method.peek("url")?.stringValue ?? '';
    paths.forEach((k, v) {
      final value = v.peek("value")?.stringValue ?? k.displayName;
      definePath = definePath.replaceFirst("{$value}", "\$${k.displayName}");
    });
    return literal(definePath);
  }

  Map<ParameterElement, ConstantReader> _getAnnotations(
      MethodElement m, Type type) {
    var annot = <ParameterElement, ConstantReader>{};
    for (final p in m.parameters) {
      final a = _typeChecker(type).firstAnnotationOf(p);
      if (a != null) {
        annot[p] = ConstantReader(a);
      }
    }
    return annot;
  }

  ConstantReader? _getMethodAnnotation(MethodElement method) {
    for (final type in _methodsAnnotations) {
      final annot = _typeChecker(type)
          .firstAnnotationOf(method, throwOnUnresolved: false);
      if (annot != null) return ConstantReader(annot);
    }
    return null;
  }

  Iterable<cb.Method?> _parseMethods(ClassElement element) {
    return element.methods.where((MethodElement m) {
      final methodAnnot = _getMethodAnnotation(m);
      return (methodAnnot != null &&
          m.isAbstract &&
          (m.returnType.isDartAsyncFuture || m.returnType.isDartAsyncStream));
    }).map((MethodElement m) {
      return _generateMethod(m);
    }).toList();
  }

  String _generateTypeParameterizedName(TypeParameterizedElement element) =>
      element.displayName +
      (element.typeParameters.isNotEmpty
          ? '<${element.typeParameters.join(',')}>'
          : '');

  Field _buildBaseUrlFiled(String url) => Field((m) => m
    ..name = 'host'
    ..type = refer("String?")
    ..modifier = FieldModifier.var$);

  String _implementClass(ele.ClassElement element, ConstantReader annotation,
      BuildStep buildStep) {
    String? host;
    try {
      host = annotation.peek('host')?.stringValue;
    } catch (e) {
      host = '';
    }
    final classBuilder = Class((c) {
      c
        ..name = 'ApiFactory'
        ..types.addAll(element.typeParameters.map((e) => refer(e.name)))
        ..constructors.add(Constructor((c) {
          c.optionalParameters.add(Parameter((p) => p
            ..named = true
            ..name = 'host'
            ..toThis = true));
          final block = [
            if (host != null && host.isNotEmpty)
              Code("host ??= ${literal(host)};"),
          ];
          c.body = Block.of(block);
        }))
        ..fields.addAll([_buildBaseUrlFiled(host!)])
        ..methods.addAll(_parseMethods(element))
        ..extend = Reference(_generateTypeParameterizedName(element));
    });
    final emitter = DartEmitter();
    return DartFormatter().format('${classBuilder.accept(emitter)}');
  }

  String _import(path) {
    var noImport = [
      "import 'package:dio/dio.dart' hide Headers;",
    ];
    var defaultImport = [
      "import 'dart:io';",
      "import 'dart:convert';",
      "import 'package:dio/dio.dart';",
      "import 'package:xanno/xanno.dart';",
      "import 'api_interface.api.dart';",
      "import 'entity/base_entity.api.dart';",
      "import 'manager/http_manager.api.dart';",
      "import 'package:http_parser/http_parser.dart' show MediaType;",
    ];
    var sb = StringBuffer();
    try {
      List<String> lines = File(path).readAsLinesSync();
      lines.forEach((v) {
        String? find;
        try {
          find = noImport.firstWhere((element) => element == v);
        } catch (e) {}
        defaultImport.removeWhere((element) => element == v);
        if (v.startsWith('import') && find == null) {
          sb.writeln(v);
        }
      });
      defaultImport.forEach((v) {
        sb.writeln(v);
      });
    } catch (e) {
      stdout.writeln('文件读取失败! $e');
    }
    return sb.toString();
  }

  @override
  generateForAnnotatedElement(
      ele.Element element, ConstantReader annotation, BuildStep buildStep) {
    if (element.kind == ele.ElementKind.CLASS) {
      String clazz =
          _implementClass((element as ele.ClassElement), annotation, buildStep);
      StringBuffer sb = StringBuffer();
      sb.writeln(_import(
          '${Directory.current.path}$separator${buildStep.inputId.path}'));
      sb.write(clazz);
      String filePath =
          '${Directory.current.path}${separator}lib/api/api_factory.interface.dart';
      File(filePath)
        ..createSync(recursive: true)
        ..writeAsStringSync(sb.toString());
    } else {
      throw InvalidGenerationSourceError('只能在class中使用');
    }
    return null;
  }
}

extension DartTypeStreamAnnotation on DartType {
  bool get isDartAsyncStream {
    ClassElement? element = this.element as ele.ClassElement?;
    if (element == null) {
      return false;
    }
    return element.name == "Stream" && element.library.isDartAsync;
  }
}

String _displayString(dynamic e) {
  try {
    return e.getDisplayString(withNullability: false);
  } catch (error) {
    if (error is TypeError) {
      return e.getDisplayString();
    } else {
      rethrow;
    }
  }
}

/// Returns `$revived($args $kwargs)`, this won't have ending semi-colon (`;`).
/// [object] must not be null.
/// [object] is assumed to be a constant.
String revivedLiteral(
  Object? object, {
  DartEmitter? dartEmitter,
}) {
  dartEmitter ??= DartEmitter();

  ArgumentError.checkNotNull(object, 'object');

  Revivable? revived;
  if (object is Revivable) {
    revived = object;
  }
  if (object is DartObject) {
    revived = ConstantReader(object).revive();
  }
  if (object is ConstantReader) {
    revived = object.revive();
  }
  if (revived == null) {
    throw ArgumentError.value(object, 'object',
        'Only `Revivable`, `DartObject`, `ConstantReader` are supported values');
  }

  String instantiation = '';
  final location = revived.source.toString().split('#');

  /// If this is a class instantiation then `location[1]` will be populated
  /// with the class name
  if (location.length > 1) {
    instantiation = location[1] +
        (revived.accessor.isNotEmpty ? '.${revived.accessor}' : '');
  } else {
    /// Getters, Setters, Methods can't be declared as constants so this
    /// literal must either be a top-level constant or a static constant and
    /// can be directly accessed by `revived.accessor`
    return revived.accessor;
  }

  final args = StringBuffer();
  final kwargs = StringBuffer();
  Spec objectToSpec(DartObject? object) {
    final constant = ConstantReader(object);
    if (constant.isNull) {
      return literalNull;
    }

    if (constant.isBool) {
      return literal(constant.boolValue);
    }

    if (constant.isDouble) {
      return literal(constant.doubleValue);
    }

    if (constant.isInt) {
      return literal(constant.intValue);
    }

    if (constant.isString) {
      return literal(constant.stringValue);
    }

    if (constant.isList) {
      return literalList(constant.listValue.map(objectToSpec));
      // return literal(constant.listValue);
    }

    if (constant.isMap) {
      return literalMap(Map.fromIterables(
          constant.mapValue.keys.map(objectToSpec),
          constant.mapValue.values.map(objectToSpec)));
      // return literal(constant.mapValue);
    }

    if (constant.isSymbol) {
      return Code('Symbol(${constant.symbolValue.toString()})');
      // return literal(constant.symbolValue);
    }

    if (constant.isNull) {
      return literalNull;
    }

    if (constant.isType) {
      return refer(_displayString(constant.typeValue));
    }

    if (constant.isLiteral) {
      return literal(constant.literalValue);
    }

    /// Perhaps an object instantiation?
    /// In that case, try initializing it and remove `const` to reduce noise
    final revived = revivedLiteral(constant.revive(), dartEmitter: dartEmitter)
        .replaceFirst('const ', '');
    return Code(revived);
  }

  for (var arg in revived.positionalArguments) {
    final literalValue = objectToSpec(arg);

    args.write('${literalValue.accept(dartEmitter)},');
  }

  for (var arg in revived.namedArguments.keys) {
    final literalValue = objectToSpec(revived.namedArguments[arg]);

    kwargs.write('$arg:${literalValue.accept(dartEmitter)},');
  }

  return '$instantiation($args $kwargs)';
}

Builder httpInterfaceBuilder(BuilderOptions options) =>
    sg.LibraryBuilder(HttpInterfaceGenerator(),
        generatedExtension: ".interface.dart");
